using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using NUnit.Framework;

namespace MonoTests.System {
	public class StringTester {
		private const string ext = ".txt";
		public static string Location = "./StringTests/";
		public static bool CreateMode = false;

		private static readonly Dictionary<string, Asserts> asserts = new Dictionary<string, Asserts> ();

		public static void Assert (string id, string actual, string message = "")
		{
			string testFullName = GetTestMethodName ();

			Asserts testAsserts;
			if (!asserts.TryGetValue (testFullName, out testAsserts)) {
				testAsserts = new Asserts ();

				if (!CreateMode) {
					string filePath = Path.GetFullPath (Path.Combine (Location, testFullName + ext));
					if (!File.Exists (filePath)) {
						NUnit.Framework.Assert.Ignore (filePath + " does not exist. \n" +
													   "The file should be generated by running on .NET the same test with StringTester.CreateMode = true.");
					}

					testAsserts.Load (filePath);
				}

				asserts.Add (testFullName, testAsserts);
			}

			if (CreateMode) {
				testAsserts.AddExpected (id, actual);
				return;
			}

			if (string.IsNullOrEmpty(message))
				message = id;

			string expected = testAsserts.GetExpected (id);
			NUnit.Framework.Assert.AreEqual (expected, actual, message);
		}

		public static void Save ()
		{
			if (!CreateMode)
				return;

			foreach (var test in asserts)
				test.Value.Save (Path.Combine (Location, test.Key + ext));
		}

		public static string GetTestMethodName ()
		{
			var stackTrace = new StackTrace ();
			foreach (StackFrame stackFrame in stackTrace.GetFrames ()) {
				MethodBase methodBase = stackFrame.GetMethod ();
				Object [] attributes = methodBase.GetCustomAttributes (typeof (TestAttribute), false);
				if (attributes.Length >= 1)
					return methodBase.DeclaringType.FullName + "." + methodBase.Name;
			}
			return "Not called from a test method";
		}

		public class Asserts {
			private const string escapes = "\n\t\0\r\\";
			private const string unescapes = @"\n\t\0\r\\";
			private static readonly Regex regex = new Regex (@"\\u(?<Value>[a-zA-Z0-9]{4})", RegexOptions.Compiled);
			private readonly Dictionary<string, string> values = new Dictionary<string, string> ();

			public void AddExpected (string id, string value)
			{
				values.Add (id, value);
			}

			public string GetExpected (string id)
			{
				return values [id];
			}

			public void Save (string filePath)
			{
				string dir = Path.GetDirectoryName (filePath);
				if (!Directory.Exists (dir))
					Directory.CreateDirectory (dir);

				var sw = new StreamWriter (filePath, false, Encoding.UTF8);

				foreach (var kv in values) {
					sw.WriteLine (Escape (kv.Key));
					sw.WriteLine (Escape (kv.Value));
				}

				sw.Close ();
			}

			public void Load (string filePath)
			{
				if (!File.Exists (filePath))
					return;

				var sr = new StreamReader (filePath, Encoding.UTF8);

				while (sr.Peek () > 0) {
					string id = Unescape (sr.ReadLine ());

					if (sr.Peek () == 0)
						break;

					string value = Unescape (sr.ReadLine ());
					values.Add (id, value);
				}

				sr.Close ();
			}

			private static string Escape (string str)
			{
				var sb = new StringBuilder ();
				foreach (char c in str) {
					int i = escapes.IndexOf (c);
					if (i != -1) {
						sb.Append (unescapes.Substring (i*2, 2));
						continue;
					}

					if (c >= 0x7f || c < 0x20) {
						sb.Append (string.Format (@"\u{0:x4}", (int) c));
						continue;
					}

					sb.Append (c);
				}
				return sb.ToString ();
			}

			private static string Unescape (string str)
			{
				for (int i = 0; i < escapes.Length; i++)
					str = str.Replace (unescapes.Substring (i*2, 2), "" + escapes [i]);

				return regex.Replace (str,
					m => ((char) int.Parse (m.Groups ["Value"].Value, NumberStyles.HexNumber)).ToString ());
			}
		}
	}
}